/*
 * Copyright (C) 2012  Forklabs Daniel Léonard
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package ca.forklabs.wow.net;

import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import ca.forklabs.baselib.io.FileUtil;

import java.io.IOException;
import java.security.GeneralSecurityException;

import static ca.forklabs.wow.net.WoWAPI.WORLD_OF_WARCRAFT_BEGINS;

/**
 * Class {@code BNetConnection} provides an easy way to access
 * <em>Battle.net</em> online RESTful resources.
 * <p>
 * This class is stateless and can be used as a global connection factory for
 * the whole duration of your application.
 * <p>
 * Its main entry points are the {@code openConnection()} and
 * {@code openSecureConnection} methods.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.wow.net.BNetConnection">Daniel Léonard</a>
 * @version $Revision$
 * @see   <a href="http://blizzard.github.com/api-wow-docs/">http://blizzard.github.com/api-wow-docs/</a>
 */
public class BNetConnection {

//---------------------------
// Instance methods
//---------------------------

   /** The date format for the signature. */
   //Fri, 10 Jun 2011 21:37:34 GMT
   private DateFormat date_format = new SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss zzz", Locale.US); //$NON-NLS-1$
   {
      TimeZone gmt = TimeZone.getTimeZone("GMT"); //$NON-NLS-1$
      this.date_format.setTimeZone(gmt);
   }


//---------------------------
// Constructors
//---------------------------

   /**
    * Constructor.
    */
   public BNetConnection() {
   // nothing
      }


//---------------------------
// Accessors and mutators
//---------------------------

   /**
    * Gets the date format.
    * @return   the date format.
    */
   protected DateFormat getDateFormat() {
      return this.date_format;
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Creates the URL to access the resource.
    * @param   secure   indicates if the communicate should be secure or not.
    * @param   region   the <em>Battle.net</em> region.
    * @param   resource   the resource.
    * @return   the URL to the resource.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   protected URL createURL(boolean secure, Region region, String resource) throws IOException {
      String protocol = (secure) ? "https" : "http";  //$NON-NLS-1$ //$NON-NLS-2$
      URL url = new URL(protocol, region.hostname(), resource);
      return url;
      }

   /**
    * Opens a connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   public HttpURLConnection openConnection(Region region, String resource) throws IOException {
      long last_modified = WORLD_OF_WARCRAFT_BEGINS;
      HttpURLConnection connection = this.openConnection(region, resource, last_modified);
      return connection;
      }

   /**
    * Opens a secure connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   public HttpURLConnection openSecureConnection(Region region, String resource) throws IOException {
      long last_modified = WORLD_OF_WARCRAFT_BEGINS;
      HttpURLConnection connection = this.openSecureConnection(region, resource, last_modified);
      return connection;
      }

   /**
    * Opens a connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   last_modified   the last modified value.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   public HttpURLConnection openConnection(Region region, String resource, long last_modified) throws IOException {
      boolean secure = false;
      HttpURLConnection connection = this.openConnection(secure, region, resource, last_modified);
      return connection;
      }

   /**
    * Opens a secure connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   last_modified   the last modified value.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   public HttpURLConnection openSecureConnection(Region region, String resource, long last_modified) throws IOException {
      boolean secure = true;
      HttpURLConnection connection = this.openConnection(secure, region, resource, last_modified);
      return connection;
      }

   /**
    * Opens a connection.
    * @param   secure   indicates if the communicate should be secure or not.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   last_modified   the last modified value.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    */
   public HttpURLConnection openConnection(boolean secure, Region region, String resource, long last_modified) throws IOException {
      URL url = this.createURL(secure, region, resource);

      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// BUG: issue 4573 of appengine http://code.google.com/p/googleappengine/issues/detail?id=4573 about last modified not going through
      connection.setIfModifiedSince(last_modified);

      return connection;
      }

   /**
    * Opens a signed and secure connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   public_key   the public key.
    * @param   private_key   the private key.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    * @throws   GeneralSecurityException   if something goes wrong with the
    *                                      signature.
    */
   public HttpURLConnection openConnection(Region region, String resource, String public_key, String private_key) throws IOException, GeneralSecurityException {
      long last_modified = WORLD_OF_WARCRAFT_BEGINS;
      HttpURLConnection connection = this.openConnection(region, resource, last_modified, public_key, private_key);
      return connection;
      }

   /**
    * Formats the date for the authorization header.
    * @param   timestamp   the timestamp.
    * @return   a readable date.
    */
   protected String format(long timestamp) {
      DateFormat format = this.getDateFormat();
      String date = format.format(new Date(timestamp));
      return date;
      }

   /**
    * Removes the query string and leaves only the path.
    * @param   resource
    * @return   the path.
    */
   protected String removeQueryString(String resource) {
      String path = resource;

      int query_string_start = resource.indexOf('?');
      if (-1 != query_string_start) {
         path = path.substring(0, query_string_start);
         }

      return path;
      }

   /** The BASE64 mapping. */
   private static final char[] base64Chars = new char[] {
// from class com.google.gwt.user.server.Base64Utils, the last two characters have been changed
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
      'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
      'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
      'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
      '4', '5', '6', '7', '8', '9', '+', '/'
      };

   /**
    * Converts a byte array into a base 64 encoded string. Null is encoded as
    * null, and an empty array is encoded as an empty string. Otherwise, the byte
    * data is read 3 bytes at a time, with bytes off the end of the array padded
    * with zeros. Each 24-bit chunk is encoded as 4 characters from the sequence
    * [A-Za-z0-9$_]. If one of the source positions consists entirely of padding
    * zeros, an '=' character is used instead.
    *
    * @param   bytes   a byte array, which may be null or empty
    * @return a String
    */
   protected String base64(byte[] bytes) {
// from class com.google.gwt.user.server.Base64Utils
      if (bytes == null) {
         return null;
         }

      int len = bytes.length;
      if (len == 0) {
         return ""; //$NON-NLS-1$
         }

      int olen = 4 * ((len + 2) / 3);
      char[] chars = new char[olen];

      int iidx = 0;
      int oidx = 0;
      int charsLeft = len;
      while (charsLeft > 0) {
         int b0 = bytes[iidx++] & 0xff;
         int b1 = (charsLeft > 1) ? bytes[iidx++] & 0xff : 0;
         int b2 = (charsLeft > 2) ? bytes[iidx++] & 0xff : 0;
         int b24 = (b0 << 16) | (b1 << 8) | b2;

         int c0 = (b24 >> 18) & 0x3f;
         int c1 = (b24 >> 12) & 0x3f;
         int c2 = (b24 >> 6) & 0x3f;
         int c3 = b24 & 0x3f;

         chars[oidx++] = base64Chars[c0];
         chars[oidx++] = base64Chars[c1];
         chars[oidx++] = (charsLeft > 1) ? base64Chars[c2] : '=';
         chars[oidx++] = (charsLeft > 2) ? base64Chars[c3] : '=';

         charsLeft -= 3;
         }

      return new String(chars);
      }

   /**
    * Creates the authorization value.
    * @param   resource   the resource.
    * @param   timestamp   the timestamp.
    * @param   public_key   the public key.
    * @param   private_key   the private key.
    * @return   the authorization value.
    * @throws   GeneralSecurityException   if something goes wrong with the
    *                                      Java Cryptographic Extension.
    * @see   <a href="http://blizzard.github.com/api-wow-docs/#id3379927">http://blizzard.github.com/api-wow-docs/#id3379927</a>
    */
   protected String authorize(String resource, long timestamp, String public_key, String private_key) throws GeneralSecurityException {
      String algorithm = "HmacSHA1"; //$NON-NLS-1$
      Charset utf8 = FileUtil.UTF_8;

      byte[] secret_bytes = private_key.getBytes(utf8);
      SecretKey secret = new SecretKeySpec(secret_bytes, algorithm);

      String verb = "GET"; //$NON-NLS-1$
      String date = this.format(timestamp);
      String path = this.removeQueryString(resource);
      String text = verb + "\n" + //$NON-NLS-1$
                    date + "\n" + //$NON-NLS-1$
                    path + "\n"; //$NON-NLS-1$
      byte[] text_bytes = text.getBytes(utf8);

      Mac mac = Mac.getInstance(algorithm);
      mac.init(secret);
      mac.update(text_bytes);
      byte[] signature_bytes = mac.doFinal();

      String signature = this.base64(signature_bytes);

      String header = "BNET " + public_key + ":" + signature;  //$NON-NLS-1$ //$NON-NLS-2$
      return header;
      }

   /**
    * Opens a signed and secure connection.
    * @param   region   the region.
    * @param   resource   the resource.
    * @param   last_modified   the last modified value.
    * @param   public_key   the public key.
    * @param   private_key   the private key.
    * @return   an URL connection.
    * @throws   IOException   if anything goes wrong with I/O.
    * @throws   GeneralSecurityException   if something goes wrong with the
    *                                      signature.
    */
   public HttpURLConnection openConnection(Region region, String resource, long last_modified, String public_key, String private_key) throws IOException,   GeneralSecurityException {
      boolean secure = true;
      HttpURLConnection connection = this.openConnection(secure, region, resource, last_modified);

      long timestamp = System.currentTimeMillis();

      String date = this.format(timestamp);
      connection.addRequestProperty("Date", date); //$NON-NLS-1$

      String authorization = this.authorize(resource, timestamp, public_key, private_key);
      connection.addRequestProperty("Authorization", authorization); //$NON-NLS-1$

      return connection;
      }

   }
